﻿using System;
using System.Globalization;
using COL = System.Collections.Generic;
using SOCK = System.Net.Sockets;
using TXT = System.Text;

namespace BoresSoft.Ftp
{
	/// <summary>
	/// Base class for FtpClient. Implements low level operations with TCP sockets.
	/// </summary>
    public class FtpClientRaw : IDisposable
    {
        private SOCK.TcpClient tcpClient = new SOCK.TcpClient();
        private SOCK.NetworkStream tcpNs;

		/// <summary>
		/// Default constructor.
		/// </summary>
        protected FtpClientRaw()
        {
        }

		/// <summary>
		/// Connects to remote FTP server.
		/// </summary>
		/// <param name="host">Host name or IP address.</param>
		/// <param name="port">Port number.</param>
		/// <returns>Server welcome message.</returns>
        protected COL.IList<ResponseLine> Connect(string host, int port)
        {
            if (null == tcpClient)
                throw new ObjectDisposedException("FtpClient");

            tcpClient.Connect(host, port);
            tcpNs = tcpClient.GetStream();

            // Read welcome message
            return SendCommand(true);
        }

		/// <summary>
		/// Connects to remote FTP server.
		/// </summary>
		/// <param name="host">Host name or IP address.</param>
		/// <returns>Server welcome message.</returns>
        public COL.IList<ResponseLine> Connect(string host)
        {
            return Connect(host, 21);
        }

		/// <summary>
		/// Indicats the connection status.
		/// </summary>
        public bool Connected
        {
            get
            {
                System.Diagnostics.Debug.Assert(!tcpClient.Client.Connected || null != tcpNs);
                return tcpClient.Client.Connected;
            }
        }

		/// <summary>
		/// Reads server response.
		/// </summary>
		/// <param name="waitResponse">Wait for response if it is not avaliable or return immediately.</param>
		/// <returns>Server response.</returns>
		protected COL.IList<ResponseLine> SendCommand(bool waitResponse)
		{
			return SendCommand(null, waitResponse);
		}

		/// <summary>
		/// Sends FTP command to server and reads response.
		/// </summary>
		/// <param name="command">FTP command.</param>
		/// <returns>Server response.</returns>
		protected COL.IList<ResponseLine> SendCommand(string command)
		{
			if (null == command || 0 == command.Length)
				throw new ArgumentException("This overloading should not be used for reading only.", "command");

			return SendCommand(command, true);
		}

		/// <summary>
		/// Sends FTP command to server and reads response.
		/// </summary>
		/// <param name="command">FTP command.</param>
		/// <param name="waitResponse">Wait for response if it is not avaliable or return immediately.</param>
		/// <returns>Server response.</returns>
        private COL.IList<ResponseLine> SendCommand(string command, bool waitResponse)
        {
			COL.List<ResponseLine> retVal = new COL.List<ResponseLine>(8);

            // If command text is specified - send it
            if (null != command && command.Length > 0)
            {
                byte[] tmp = TXT.Encoding.ASCII.GetBytes(command + "\r\n");
                tcpNs.Write(tmp, 0, tmp.Length);
            }
			else if (!waitResponse && !tcpNs.DataAvailable)
			{
				// If no command specified - just read the response
				// If no response avaliable - return emmediately
				return retVal;
			}

            // Reading server response
            byte nlIndx = 0;
            bool lastLine = false;
            TXT.StringBuilder sbCode = new TXT.StringBuilder(4), sbOut = new TXT.StringBuilder(32);
            byte[] buff = new byte[512]; // Should be enough for response line

            do
            {
                int bytesRead = tcpNs.Read(buff, 0, buff.Length);
                for (int b = 0; b < bytesRead; b++)
                {
                    char ch = TXT.Encoding.ASCII.GetChars(new byte[] { buff[b] })[0];
                    if (nlIndx < 3)
                    { // It's a response code
                        sbCode.Append(ch);
                        nlIndx++;
                    }
                    else if (3 == nlIndx)
                    { // It's a marker
                        if (' ' == ch)
                            lastLine = true;
                        else if ('-' == ch)
                            lastLine = false;
                        else
                            throw new FtpException(ErrorCode.InvalidMarker);

                        nlIndx++;
                    }
                    else
                    {
                        if ('\n' == ch) // EOL
                        {
                            // Add all to the output
                            try
                            {
                                retVal.Add(
                                    new ResponseLine(
                                        short.Parse(sbCode.ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture),
                                        sbOut.ToString().Trim()
                                    )
                                );
                            }
                            catch (FormatException)
                            {
                                throw new FtpException(ErrorCode.InvalidResponse);
                            }

                            // Clear buffers
                            sbCode.Remove(0, sbCode.Length);
                            sbOut.Remove(0, sbOut.Length);

                            nlIndx = 0;
                        }

                        sbOut.Append(ch);
                    }
                }
            }
            while (tcpNs.DataAvailable || !lastLine);

            return retVal;
        }

		/// <summary>
		/// 
		/// </summary>
		/// <param name="disposing"></param>
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
				if (null != tcpNs)
				{
					tcpNs.Close();
					tcpNs = null;
				}

                if (null != tcpClient)
                {
					tcpClient.Close();
					tcpClient = null;
                }
            }
        }

        #region IDisposable Members

		/// <summary>
		/// 
		/// </summary>
		public void Dispose()
		{
			Dispose(true);
			GC.SuppressFinalize(this);
		}

		#endregion

		/// <summary>
		/// 
		/// </summary>
		~FtpClientRaw()
		{
			Dispose(false);
		}
    }
}
